/*-
 * KeywordProcessor.java
 *
 * $LastChangedBy: stein $
 * $LastChangedRevision: 95 $
 * $LastChangedDate: 2007-02-13 16:19:24 +0000 (Tue, 13 Feb 2007) $
 * $HeadURL: svn://localhost/SMS/src/com/googlecode/attention/sms/comp/KeywordProcessor.java $
 *
 * Copyright (c) 2007, Stein Gunnar Bakkeby
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Stein Gunnar Bakkeby nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.googlecode.attention.sms.comp;

import static com.googlecode.attention.sms.SMSLogStatements.*;
import static com.googlecode.attention.util.Convert.*;
import static com.googlecode.attention.util.SMSCharacterSet.*;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.FileNotFoundException;
import java.io.IOException;

import com.googlecode.attention.sms.pdu.CBS;
import com.googlecode.attention.util.IntArrayBuilder;
import com.googlecode.attention.util.Settings;

/** * Keyword Processor used to Compress/Decompress user data. * <BR>See 3GPP TS 23.042 Version 5.0.0 Release 5 *  * @author Stein Gunnar Bakkeby */
public class KeywordProcessor {

  /* Field values */
  private MatchOptions match_options;
  private IntArrayBuilder keyword_prefix;
  private IntArrayBuilder keyword_suffix;
  private IntArrayBuilder[] keyword_group_list;
  private int keyword_threshold;
  private int maximum_partial_match_length;
  private CH previousCH = null;
  private int kp_id = 0;
  private int numberOfProcessedBits = 0;
  
  /* Static field values */
  // TODO consider enums
  /** Exact match. */
  public static final int EXACT_MATCH     = 0;
  /** Lower case match. */
  public static final int LOWER_MATCH     = 1;
  /** Upper case match. */
  public static final int UPPER_MATCH     = 2;
  /** First char upper case followed by remainder lower case match. */
  public static final int UPPER_1ST_MATCH = 3;
  /** Partial match. */
  public static final int PARTIAL_MATCH   = 4;
  /** Partial lower case match. */
  public static final int PARTIAL_LOWER_MATCH = 5;
  /** Partial upper case match. */
  public static final int PARTIAL_UPPER_MATCH = 6;
  /**
   * Partial match with first char upper case
   * followeed by remainder lower case.
   */ 
  public static final int PARTIAL_UPPER_1ST_MATCH = 7;
  /* End of static field values */
  
  /**
   * Constructor, creates the Keyword processor based 
   * @param ch The compression header
   * @throws IOException If initialisation file cannot be read
   */
  public KeywordProcessor(final CH ch) throws IOException {
    changeInitialisation(ch);
  }
  
  /**
   * This will change the Keyword initialisation parameters.
   * 
   * @param ch The compression header
   * @throws IOException If initialisation file cannot be read
   */
  public void changeInitialisation(final CH ch) throws IOException {
    if (previousCH == null
        || previousCH.isUpdateNeeded(ch, CH.CHANGE_KEYWORD_DICTIONARY)) {
      final String lang = CBS.parse(ch.getCLC()).toString().toLowerCase();
      /* If change initialisation ID */
      if (ch.getExtensionValue(CH.CHANGE_KEYWORD_DICTIONARY) != -1)
          kp_id = ch.getExtensionValue(CH.CHANGE_KEYWORD_DICTIONARY);
      String path = this.getClass().getCanonicalName();
      path = path.substring(0, path.lastIndexOf("."));
      final String resource = path + "."
                            + lang
                            + "_keyword_"
                            + kp_id;
      
      if (kp_id == 0) {
        match_options  = new MatchOptions();
        keyword_prefix = new IntArrayBuilder(new int[0]);
        keyword_suffix = new IntArrayBuilder(new int[0]);
        keyword_threshold = 0;
        maximum_partial_match_length = 0;
        keyword_group_list = new IntArrayBuilder[0];
      } else {    
        final Settings s = new Settings(resource);
        
        final String binString = s.getString("match_options");
        match_options     = new MatchOptions(com.googlecode.attention.util.Convert.binToIntArray(binString));
        keyword_prefix    = new IntArrayBuilder(s.getString("keyword_prefix").replace("<SP>", " "));
        keyword_suffix    = new IntArrayBuilder(s.getString("keyword_suffix").replace("<SP>", " "));
        keyword_threshold = s.getInteger("keyword_threshold");
        maximum_partial_match_length = s.getInteger("maximum_partial_match_length");
         
        final String[] files = s.getList("keyword_group_list_");
        for (int i = 0; i < files.length; i++) {
          String x = files[i];
          x = x.replace("<lang>", lang);
          x = x.replace("<id>", "" + kp_id);
          files[i] = x;
        }
        final IntArrayBuilder[] data = readInitFile(files);
        if (previousCH != null &&
            ch.getExtensionValue(CH.CHANGE_CHARACTER_SET)
            != previousCH.getExtensionValue(CH.CHANGE_CHARACTER_SET)) {
          for (int i = 0; i < data.length; i++) {
            data[i] = changeCharacterSet(
                data[i],
                ch.getExtensionValue(CH.CHANGE_CHARACTER_SET),
                previousCH.getExtensionValue(CH.CHANGE_CHARACTER_SET));
          }
        }
        keyword_group_list = data;
      }
    }
    previousCH = new CH(ch);
  }
  
  /**
   * This will read a given input/init file.
   * 
   * @param file The file to read from
   * @return An array of the strings retrieved from file
   */
  public IntArrayBuilder[] readInitFile(final String file) {
    return readInitFile(new String[] {file});
  }
  
  /**
   * This will read the given input/init files.
   * 
   * @param files The files to read from
   * @return An array of the strings retrieved from files
   */
  public IntArrayBuilder[] readInitFile(final String[] files) {
    int max_length = 512; // init length > most dictionaries
    String[] tmp = new String[max_length];
    int entriesNum = 0;
    
    /* Generate the dictionary */
    /* Read dictionary entries from file(s) --> tmp */
    final int length = files.length;
    for (int i = 0; i < length; i++) { // until exception       
      try {
        final BufferedReader reader = new BufferedReader(new InputStreamReader(
            ClassLoader.getSystemResourceAsStream(files[i])));
        String line = "";
        for (int j = 0; line != null; j++) {
          line = reader.readLine();
          if (line == null) break;
          if (j >= max_length) { // double temporary dictionary storage
            String[] tmp2 = new String[max_length * 2];
            for (int k = 0; k < max_length; k++) {
              tmp2[k] = tmp[k];
            }
            max_length *= 2;
            tmp = tmp2;
          }
          if (!line.equals("")) { // skip empty lines
            tmp[j] = line;
            entriesNum++;
          }
        }
        reader.close();
      } catch (FileNotFoundException e) {
        SMS_DEBUG.log(e);
      } catch (IOException e) {
        SMS_DEBUG.log(e);
      }
    }
    
    /* Sort dictionary */
    IntArrayBuilder[] keyword_group_list = new IntArrayBuilder[entriesNum];
    for (int i = 0; i < entriesNum; i++) {
      for (int j = i + 1; j < entriesNum; j++) {
        if (tmp[i].compareTo(tmp[j]) > 0) {
          String temp = tmp[i];
          tmp[i] = tmp[j];
          tmp[j] = temp;
        }
      }
      keyword_group_list[i] = new IntArrayBuilder(tmp[i].replaceAll("<SP>", " "));
    }
    return keyword_group_list;
  }
  

  
  /**
   * This will use the keyword processor to compress data.
   * 
   * @param data The user data
   * @param index An offset into the input stream of characters
   * @return The keyword match
   */
  public KeywordMatch compress(final int[] data, final int index) {
    return compress(new IntArrayBuilder(data), index);
  }
  
  /**
   * This will use the keyword processor to compress data.
   * 
   * @param data The user data
   * @param index An offset into the input stream of characters
   * @return The keyword match
   */
  public KeywordMatch compress(final IntArrayBuilder data, int index) {
    /* Step 1: Set the current char pos to the input offset: e.g. index */
    /* Step 2 */
    boolean prefix_found = false;
    if (match_options.isPrefixMatchEnabled()) {
      if (data.startsWith(keyword_prefix)) {
        prefix_found = true;
        index += keyword_prefix.length();
      }
    }
    
    /* Step 3 */
    boolean match_found = false;
    int longestMatch = 0;
    int longestID = 0;
    int longestType = 0;
    int dictLength = keyword_group_list.length;
    
    /* Find longest exact match if enabled */
    IntArrayBuilder tmpData = data.substring(index);
    if (match_options.isExactMatchEnabled()) {
      for (int i = 0; i < dictLength; i++) {
        if (tmpData.startsWith(keyword_group_list[i])) {
          int length = keyword_group_list[i].length();
          if (length >= longestMatch && length >= keyword_threshold) {
            assert (i > longestID) : "Should always be true as dict is sorted";
            longestMatch = length;
            longestID    = i;
            longestType  = EXACT_MATCH;
          }
        }
      }
    }
    
    /* Find longest lower case match if enabled */
    boolean first = true;
    if (match_options.isLowerMatchEnabled()) {
      for (int i = 0; i < dictLength; i++) {
        if (tmpData.startsWith(keyword_group_list[i].toLowerCase())) {
          int length = keyword_group_list[i].length();
          if (length >= longestMatch && length >= keyword_threshold) {
            if (first || i > longestID) {
              first        = false;
              longestMatch = length;
              longestID    = i;
              longestType  = LOWER_MATCH;
            }
          }
        }
      }
    }
    
    /* Find longest upper case match if enabled */
    first = true;
    if (match_options.isUpperMatchEnabled()) {
      for (int i = 0; i < dictLength; i++) {
        if (tmpData.startsWith(keyword_group_list[i].toUpperCase())) {
          int length = keyword_group_list[i].length();
          if (length >= longestMatch && length >= keyword_threshold) {
            if (first || i > longestID) {
              first        = false;
              longestMatch = length;
              longestID    = i;
              longestType  = UPPER_MATCH;
            }
          }
        }
      }
    }
    
    /* Find longest upper case 1st match if enabled */
    first = true;
    if (match_options.isUpper1stMatchEnabled()) {
      final IntArrayBuilder keyword = new IntArrayBuilder();
      for (int i = 0; i < dictLength; i++) {
        if (keyword_group_list[i].length() > 0) {
          keyword.clear();
          keyword.append(Character.toUpperCase(
              keyword_group_list[i].intAt(0)));
          keyword.append(keyword_group_list[i].substring(1));
          if (tmpData.startsWith(keyword)) {
            int length = keyword.length();
            if (length >= longestMatch && length >= keyword_threshold) {
              if (first || i > longestID) {
                first        = false;
                longestMatch = length;
                longestID    = i;
                longestType  = UPPER_1ST_MATCH;
              }
            }
          }
        }
      }
    }
    
    /* Find longest partial match if enabled */
    if (match_options.isPartialMatchEnabled()) {
      for (int i = 0; i < dictLength; i++) {
        final IntArrayBuilder keyword = keyword_group_list[i];
        int length = (keyword.length() > tmpData.length()
                   ? tmpData.length()
                   : keyword.length());
         
        int currentMatch = 0;
        int currentType  = PARTIAL_MATCH;
        
        for (int j = 0; j < length; j++) {
          if (tmpData.intAt(j) == keyword.intAt(j)) continue;
          if (j > currentMatch) currentMatch = j;
          break;
        }
        
        if (match_options.isLowerMatchEnabled()) {
          final IntArrayBuilder temp = keyword.toLowerCase();
          for (int j = 0; j < length; j++) {
            if (tmpData.intAt(j) == temp.intAt(j)) continue;
            if (j > currentMatch) {
              currentMatch = j;
              currentType  = PARTIAL_LOWER_MATCH;
            }
            break;
          }
        }
        
        if (match_options.isUpperMatchEnabled()) {
          final IntArrayBuilder temp = keyword.toUpperCase();
          for (int j = 0; j < length; j++) {
            if (tmpData.intAt(j) == temp.intAt(j)) continue;
            if (j > currentMatch) {
              currentMatch = j;
              currentType  = PARTIAL_UPPER_MATCH;
            }
            break;
          }
        }
        
        if (match_options.isUpper1stMatchEnabled()) {
          if (keyword.length() > 0) {
            final IntArrayBuilder temp = new IntArrayBuilder();
            temp.append(Character.toUpperCase(keyword.intAt(0)));
            temp.append(keyword.substring(1).toLowerCase());
            for (int j = 0; j < length; j++) {
              if (tmpData.intAt(j) == temp.intAt(j)) continue;
              if (j > currentMatch) {
                currentMatch = j;
                currentType  = PARTIAL_UPPER_1ST_MATCH;
              }
              break;
            }
          }
        }
        
        /* Check if current match is better than the previous best match */
        if (currentMatch >= longestMatch
         && currentMatch >= keyword_threshold + 2) {
          if (longestType == EXACT_MATCH) {
            if (currentMatch + 2 >= longestMatch) {
              longestMatch = currentMatch > maximum_partial_match_length
                           ? maximum_partial_match_length
                           : currentMatch;
              longestType  = currentType;
              longestID    = i;
            } // else ignore
          } else {
            longestMatch = currentMatch > maximum_partial_match_length
                         ? maximum_partial_match_length
                         : currentMatch;
            longestType  = currentType;
            longestID    = i;
          }
        }
      }
      if (longestMatch != 0) match_found = true;
      index += longestMatch;
    }
    
    /* Step 4 */
    boolean suffix_found = false;
    if (match_options.isSuffixMatchEnabled() && match_found) {
      tmpData = data.substring(index);
      if (tmpData.startsWith(keyword_suffix)) {
        suffix_found = true;
        index += keyword_suffix.length();
      }
    }
    
    /* Step 5: Generate the keyword match bitstream. */
    final IntArrayBuilder output = new IntArrayBuilder();
    
    /* Bits 0 to N1: Case convertion */
    if (longestType != EXACT_MATCH) {
      switch (longestType) {
      case LOWER_MATCH:
        output.append(0);
        break;
        
      case UPPER_MATCH:
        output.append(1);
        output.append(0);
        break;
        
      case UPPER_1ST_MATCH:
        output.append(1);
        output.append(1);
        break;
      
      default:
        break;
      }
    }
    
    /* Bits N1 + 1 to N2: Keyword Entry ID */
    int minBits = intToBin(keyword_group_list.length - 1).length();
    output.append(binToIntArray(intToBin(longestID, minBits)));
    
    /* Bits N2 + 1 to N3: Prefix Match */
    if (match_options.isPrefixMatchEnabled()) {
      output.append(prefix_found ? 1 : 0);
    }
    
    /* Bits N3 + 1 to N4: Partial Match */
    if (match_options.isPartialMatchEnabled()) {
      switch (longestType) {
      case PARTIAL_MATCH:
      case PARTIAL_LOWER_MATCH:
      case PARTIAL_UPPER_MATCH:
      case PARTIAL_UPPER_1ST_MATCH:
        output.append(1);
        break;
        
      default:
        output.append(0);
        break;
      }
      
      if (longestMatch < keyword_group_list[longestID].length()) {
        if (longestMatch < 8) {
          output.append(0);
          output.append(stringToInt(intToBin(longestMatch, 3)));
        } else {
          output.append(1);
          minBits = intToBin(maximum_partial_match_length
              - (keyword_threshold + 2)).length();
          output.append(stringToInt(intToBin(longestMatch, minBits)));
        }
      }
    }
    
    /* Bits N4 + 1 to N5: Suffix Match */
    if (match_options.isSuffixMatchEnabled()) {
      output.append(suffix_found ? 1 : 0);
    }
    
    KeywordMatch match = new KeywordMatch();
    match.setBitstream(output.toArray());
    match.setCurrentIndex(index);
    match.setMatchFound(match_found);
    return match;
  }
  
  /**
   * This will use the keyword processor to decompress data.
   * 
   * @param data The compressed user data as a bitstream
   * @return The compressed data
   */
  public IntArrayBuilder decompress(final int[] data) {
    return decompress(new IntArrayBuilder(data));
  }
  
  /**
   * This will use the keyword processor to decompress data.
   * 
   * @param data The compressed user data as a bitstream
   * @return The compressed data
   */
  public IntArrayBuilder decompress(IntArrayBuilder data) {
    int case_conv = 0;
    numberOfProcessedBits = 0;
    if (!match_options.isExactMatchEnabled()) {
      int c = data.intAt(0);
      data = data.substring(1);
      if (c == 1) {
        numberOfProcessedBits = 2;
        case_conv = (data.intAt(0) == 1 ? 3 : 2);
        data = data.substring(1);
      } else {
        numberOfProcessedBits = 1;
        case_conv = 0;
      } 
    }
    
    int minBits = getNumberOfKeywordBits();
    int keyword_entry = binToInt(data.substring(0, minBits).toArray());
    data = data.substring(minBits);
    numberOfProcessedBits += minBits;
    
    boolean prefix_found = false;
    if (match_options.isPrefixMatchEnabled()) {
      prefix_found = (data.intAt(0) == 1);
      data = data.substring(1);
      numberOfProcessedBits++;
    }
    
    boolean partial_match = false;
    int partial_match_length = 0;
    if (match_options.isPartialMatchEnabled()) {
      partial_match = (data.intAt(0) == 1);
      data = data.substring(1);
      numberOfProcessedBits++;
      if (partial_match && data.length() > 1) {
        int c = data.intAt(0);
        data = data.substring(1);
        numberOfProcessedBits++;
        if (c == 0) {
          partial_match_length = binToInt(data.substring(0, 3));
          data = data.substring(3);
          numberOfProcessedBits += 3;
        } else {
          minBits = intToBin(maximum_partial_match_length
              - (keyword_threshold + 2)).length();
          partial_match_length = binToInt(data.substring(0, minBits));
          data = data.substring(minBits);
          numberOfProcessedBits += minBits;
        }
      } else if (partial_match) {
        partial_match_length = keyword_group_list[keyword_entry].length();
      }
    }
    
    boolean suffix_found = false;
    if (match_options.isSuffixMatchEnabled()) {
      suffix_found = (data.intAt(0) == 1);
      numberOfProcessedBits++;
    }
    
    /* Begin decoding */
    final IntArrayBuilder output = new IntArrayBuilder();
    /* Step 1: Prefix Match */
    if (prefix_found) output.append(keyword_prefix);
    
    /* Step 2: Keyword Match */
    IntArrayBuilder keyword = keyword_group_list[keyword_entry];
    if (partial_match) {
      keyword = keyword.substring(0, partial_match_length);
    }
    
    if (!match_options.isExactMatchEnabled()) {
      switch (case_conv) {
      case 0:
        keyword = keyword.toLowerCase();
        break;
        
      case 2:
        keyword = keyword.toUpperCase();
        break;
        
      case 3:
        IntArrayBuilder tmp = new IntArrayBuilder(keyword.length());
        tmp.append(keyword.substring(0, 1).toUpperCase());
        tmp.append(keyword.substring(1).toLowerCase());
        keyword = tmp;
        break;
        
      default:
        break;
      }
    }
    
    output.append(keyword);
    
    /* Step 3: Suffix Match */
    if (suffix_found) output.append(keyword_suffix);
    
    return output;
  }

  /**
   * This will return the number of processed bits.
   * 
   * @return Integer
   */
  public int getNumberOfProcessedBits() {
    return numberOfProcessedBits;
  }

  
  /**
   * This will return the minimum number of bits used to represent a keyword.
   * 
   * @return The number of bits used to represent a keyword
   */
  public int getNumberOfKeywordBits() {
    return intToBin(keyword_group_list.length - 1).length();
  }
}